use regex;
use std::collections::BTreeMap;
use std::error;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use std::{fs, process};

fn main() {
    let test_paths = get_test_paths().expect("no test!");
    println!("Test paths are {:?}", test_paths);
    let mut profraws: Vec<PathBuf> = vec![];

    for test_path in &test_paths {
        let test_result_path = run_tests(test_path.as_path()).expect("can't test!");
        profraws.push(test_result_path);
    }

    generate_profdata(profraws).expect("can't process!");
    show_coverage(&test_paths).expect("cannot make a report!");
}

fn pick_latest(test_paths: &Vec<PathBuf>) -> Result<&PathBuf, Box<dyn error::Error>> {
    let mut times: BTreeMap<SystemTime, &PathBuf> = BTreeMap::new();

    for path in test_paths {
        let meta = fs::metadata(path)
            .expect(&("no metadata for".to_owned() + path.to_str().expect("cannot stringify")));
        let time = meta.created().expect("a file without creation date");
        times.insert(time, path);
    }
    let (_time, &path) = times.last_key_value().expect("no profdata paths?");
    return Ok(path);
}

fn show_coverage(test_paths: &Vec<PathBuf>) -> Result<(), Box<dyn error::Error>> {
    assert!(Path::new("default.profdata").exists(), "no profdata!");

    let mut args = vec!["report", "--instr-profile", "default.profdata"];
    args.extend(
        test_paths
            .iter()
            .map(|path| ["-object", path.to_str().unwrap()])
            .flatten(),
    );

    println!("llvm-cov args: {:?}", args);

    let mut child = process::Command::new("llvm-cov")
        .args(args)
        .spawn()
        .expect("could not run llvm-cov");
    child.wait()?;
    return Ok(());
}

fn generate_profdata(paths: Vec<PathBuf>) -> Result<(), Box<dyn error::Error>> {
    println!("Generating profdata");

    let mut args = vec!["merge"];
    args.extend(paths.iter().map(|x| x.to_str().unwrap()));
    args.push("-o");
    args.push("default.profdata");

    let mut child = process::Command::new("llvm-profdata")
        .args(args)
        .spawn()
        .expect("could not process data");
    child.wait()?;

    for path in paths {
        fs::remove_file(path).expect("cannot remove a file!");
    }

    return Ok(());
}

/// Run an executable and collect your resulting profile
fn run_tests(test_path: &Path) -> Result<PathBuf, Box<dyn error::Error>> {
    let mut child = process::Command::new(test_path.to_str().unwrap())
        .spawn()
        .expect("could not run tests!");
    child.wait()?;

    let mut paths: Vec<PathBuf> = Vec::new();
    for dir in fs::read_dir(".").expect("cannot ls") {
        let entry = dir.expect("who knows, ls sometimes gets empty entries?");
        paths.push(entry.path())
    }
    let cargo_given_name = pick_latest(&paths)?;

    let newname: String =
        String::from(test_path.file_name().unwrap().to_str().unwrap()) + ".profraw";
    fs::rename(cargo_given_name, &newname).expect("cannot rename");
    return Ok(fs::canonicalize(newname).expect("could not make a profdata"));
}

/// Collect paths of test executables generated by Cargo
fn get_test_paths() -> Result<Vec<PathBuf>, Box<dyn error::Error>> {
    let child = process::Command::new("cargo")
        .env("RUSTFLAGS", "-C instrument-coverage")
        .args(["test", "--no-run"])
        .output()
        .expect("could not run cargo");
    let bytestring = child.stderr;
    let rawstring = String::from_utf8(bytestring).expect("cargo output not utf");

    // println!("{}", rawstring);
    assert_ne!(rawstring.len(), 0, "empty cargo output!");

    let parentheses = regex::Regex::new(r"\((?P<inside>.*)\)").unwrap();

    let mut result: Vec<PathBuf> = vec![];

    for line in rawstring.lines() {
        if line.contains("Executable") {
            for found_parentheses in parentheses.captures_iter(line) {
                let path = Path::new(&found_parentheses["inside"]);
                // println!("test path candidate {:?}", path);
                if path.exists() {
                    result.push(path.to_path_buf());
                    // return Ok(path.to_path_buf());
                }
            }
        }
    }

    if !result.is_empty() {
        return Ok(result);
    }

    return Err("no test path detected")?;
}
